agentmux_srv\backend\history/
index.rs1use std::collections::HashMap;
7use std::sync::Mutex;
8
9use super::adapter::*;
10
11pub struct SessionIndex {
13 sessions: Mutex<HashMap<String, SessionMeta>>,
15 adapters: Vec<Box<dyn HistoryAdapter>>,
17}
18
19impl SessionIndex {
20 pub fn new(adapters: Vec<Box<dyn HistoryAdapter>>) -> Self {
21 SessionIndex {
22 sessions: Mutex::new(HashMap::new()),
23 adapters,
24 }
25 }
26
27 pub fn refresh(&self) -> (u32, u32, u32) {
30 let mut discovered: u32 = 0;
31 let mut updated: u32 = 0;
32 let mut new_count: u32 = 0;
33
34 let mut new_sessions: HashMap<String, SessionMeta> = HashMap::new();
35
36 for adapter in &self.adapters {
37 let files = match adapter.discover_files() {
38 Ok(f) => f,
39 Err(e) => {
40 tracing::warn!(
41 "history: failed to discover {} files: {}",
42 adapter.provider(),
43 e
44 );
45 continue;
46 }
47 };
48
49 discovered += files.len() as u32;
50
51 for file in &files {
52 match adapter.extract_meta(&file.file_path) {
53 Ok(Some(meta)) => {
54 new_sessions.insert(meta.session_id.clone(), meta);
55 }
56 Ok(None) => {} Err(e) => {
58 tracing::debug!(
59 "history: failed to extract meta from {}: {}",
60 file.file_path,
61 e
62 );
63 }
64 }
65 }
66 }
67
68 let mut sessions = self.sessions.lock().unwrap();
70 for (id, _meta) in &new_sessions {
71 if sessions.contains_key(id) {
72 updated += 1;
73 } else {
74 new_count += 1;
75 }
76 }
77
78 *sessions = new_sessions;
79
80 (discovered, updated, new_count)
81 }
82
83 pub fn list(
85 &self,
86 provider: Option<&str>,
87 project: Option<&str>,
88 offset: usize,
89 limit: usize,
90 sort_by: &str,
91 sort_dir: &str,
92 ) -> (Vec<SessionMeta>, u32, bool) {
93 let sessions = self.sessions.lock().unwrap();
94
95 let mut filtered: Vec<&SessionMeta> = sessions
96 .values()
97 .filter(|s| {
98 if let Some(p) = provider {
99 if s.provider != p {
100 return false;
101 }
102 }
103 if let Some(proj) = project {
104 if !s.working_directory.contains(proj) {
105 return false;
106 }
107 }
108 true
109 })
110 .collect();
111
112 let desc = sort_dir != "asc";
114 match sort_by {
115 "created_at" | "created" => {
116 filtered.sort_by(|a, b| {
117 if desc {
118 b.created_at.cmp(&a.created_at)
119 } else {
120 a.created_at.cmp(&b.created_at)
121 }
122 });
123 }
124 "messages" => {
125 filtered.sort_by(|a, b| {
126 if desc {
127 b.message_count.cmp(&a.message_count)
128 } else {
129 a.message_count.cmp(&b.message_count)
130 }
131 });
132 }
133 "tokens" => {
134 filtered.sort_by(|a, b| {
135 if desc {
136 b.total_tokens.cmp(&a.total_tokens)
137 } else {
138 a.total_tokens.cmp(&b.total_tokens)
139 }
140 });
141 }
142 _ => {
143 filtered.sort_by(|a, b| {
145 if desc {
146 b.modified_at.cmp(&a.modified_at)
147 } else {
148 a.modified_at.cmp(&b.modified_at)
149 }
150 });
151 }
152 }
153
154 let total = filtered.len() as u32;
155 let has_more = offset + limit < filtered.len();
156 let page: Vec<SessionMeta> = filtered
157 .into_iter()
158 .skip(offset)
159 .take(limit)
160 .cloned()
161 .collect();
162
163 (page, total, has_more)
164 }
165
166 pub fn get_meta(&self, session_id: &str) -> Option<SessionMeta> {
168 let sessions = self.sessions.lock().unwrap();
169 sessions.get(session_id).cloned()
170 }
171
172 pub fn get_full(&self, session_id: &str) -> Result<Option<HistorySession>, HistoryError> {
174 let meta = match self.get_meta(session_id) {
175 Some(m) => m,
176 None => return Ok(None),
177 };
178
179 for adapter in &self.adapters {
181 if adapter.provider() == meta.provider {
182 return adapter.parse_file(&meta.file_path);
183 }
184 }
185
186 Err(HistoryError::Other(format!(
187 "no adapter for provider: {}",
188 meta.provider
189 )))
190 }
191
192 pub fn is_empty(&self) -> bool {
194 self.sessions.lock().unwrap().is_empty()
195 }
196}